const pathUtil = require('path');
const fs = require('fs');
const rimraf = require("rimraf");

const webpack = require('webpack');
const {
	ConcatSource,
	RawSource,
	PrefixSource,
	OriginalSource,
	LineToLineMappedSource,
} = require('webpack-sources');
const MultiEntryPlugin = require('webpack/lib/MultiEntryPlugin');
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');


const _ = require('lodash');
const {
	readJsonSync,
	filterExts,
	replaceDS,
	removeExt,
	filterExcludeItems,
	warn, error, highLight, right,
} = require('./utils');

const MiniProgramResources = require('./miniprogram-resources');

const {exit} = process;

class MiniProgramPlugin {

	constructor(srcDir, distDir, options, isBreakInPrepare) {
		this.__isBreakInPrepare = !!isBreakInPrepare;
		this.__modulePath = __filename;
		this.__moduleDir = pathUtil.dirname(__filename);

		this.__isPrepare = false;
		this.webpackContext = null;
		this.webpackEntryScripts = {};
		this.webpackEntryAssets = {};
		this.webpackEntryScriptChunks = {};
		this.webpackExclude = {
			count:   0,
			chunks:  [],
			scripts: [],
			assets:  [],
		};

		this.root = process.cwd();

		this.srcDir = srcDir;
		this.srcPath = pathUtil.resolve(this.root, srcDir);
		this.distDir = distDir;
		this.distPath = pathUtil.resolve(this.root, distDir);

		this.pluginName = 'MiniProgramWebpackPlugin';
		this.options = {
			main:                'app.json',
			mockMain:            false,
			debug:               false,
			assets:              'assets',
			assetsChunkName:     '__assets_chunk_name__',
			bootstrapModuleName: 'bootstrap.js',
			exts:                {
				json: true,
				wxml: true,
				wxss: false,
			},
			scriptExts:          {js: true},
			checkStatExts:       {wxss: true},
			app:                 {
				exts:  {wxml: false},
				files: [
					'project.config.json',
				],
			},
			page:                {
				// exts: {wxml: true},
				atta: [],
			},
			component:           {
				// exts: {wxml: true},
				atta: [],
			},
			custom:              {
				// 'app': {
				// 	exts: {styl: true, wxss: false},
				// 	atta: []
				// }
				// 'pages/index/index': {
				// 	exts: [],
				// 	atta: ['sitemap.json', 'project.config.js'],
				// },
			},
		};

		if (_.isObjectLike(options)) {
			this.options = _.merge(this.options, options);
		}
	}

	prepare(compiler, context, entry) {
		if (this.__isPrepare) return this;

		const {options} = compiler;
		const {output} = options;
		if (output.libraryTarget !== 'var')
			throw new Error(`MiniProgram webpack plugin must set as output.libraryTarget = 'var'`);

		this.webpackContext = pathUtil.resolve(context);

		this.initWebpackEntry(entry);

		if (!!this.__isBreakInPrepare) {
			console.log(`MiniProgram webpack plugin (${highLight(this.__moduleDir)}) exit in prepare `);
			console.log(this.options);
			exit();
		}

		return this;
	}

	apply(compiler) {
		let isFirst = true;

		compiler.hooks.entryOption.tap(this.pluginName, (context, entry) => {
			this.processTip('compiler.hooks.entryOption');
			this.prepare(compiler, context, entry);
		});

		compiler.hooks.beforeCompile.tapAsync(this.pluginName, (compilation, callback) => {
			this.processTip('compiler.hooks.beforeCompile');
			callback();
		});

		compiler.hooks.thisCompilation.tap(this.pluginName, (compilation, params) => {
			this.processTip('compiler.hooks.thisCompilation');
			const {remove} = this.newResources().seekApp().diffEntries();
			this.webpackExclude = remove; // 重新写入一次
		});

		compiler.hooks.compilation.tap(this.pluginName, (compilation, params) => {
			this.processTip('compiler.hooks.compilation');

			const findChunkIndex = (chunkName) => compilation.chunks.findIndex(
				({name}) => name === chunkName,
			);

			const excludeChunk = (chunkName) => {
				const chunkIndex = findChunkIndex(chunkName);
				if (chunkIndex > -1) {
					compilation.chunks.splice(chunkIndex, 1);
					if (this.webpackEntryScriptChunks[chunkName])
						this.processTip(`excludeEntryScript`, error(chunkName));
					else
						this.processTip(`excludeChunk`, error(chunkName));
				}
			};

			compilation.hooks.beforeChunkAssets.tap(this.pluginName, () => {
				this.processTip('compilation.hooks.beforeChunkAssets');
				excludeChunk(this.options.assetsChunkName);
			});

			compilation.hooks.optimizeChunksBasic.tap(this.pluginName, (chunks, chunkGroups) => {
				this.processTip('compilation.hooks.optimizeChunksBasic');
				if (this.webpackExclude.count > 0) {
					if (this.webpackExclude.chunks.length > 0) {
						this.webpackExclude.chunks.forEach(chunkName => {
							excludeChunk(chunkName);
						});
					}
				}
			});

			compilation.hooks.additionalAssets.tapAsync(this.pluginName, callback => {
				// 这里附加的内容不在 entry
				compilation.assets[this.options.bootstrapModuleName] = new ConcatSource(
					fs.readFileSync(pathUtil.resolve(this.__moduleDir, './miniprogram-bootstrap.js'), 'utf8'),
				);
				callback();
			});

			compilation.hooks.optimizeAssets.tap(this.pluginName, (assets) => {
				this.processTip('compilation.hooks.optimizeAssets');
				if (this.webpackExclude.assets.length > 0) {
					this.webpackExclude.assets.forEach(key => {
						if (assets[key]) {
							this.processTip(`excludeAsset`, error(key));
							delete (assets[key]);
						}
					});
				}
			});

			compilation.hooks.needAdditionalPass.tap(this.pluginName, () => {
				this.processTip('compilation.hooks.needAdditionalPass');
				const {add, remove} = this.resources.diffEntries();
				return add.count > 0;
			});

			compilation.mainTemplate.hooks.render.tap('MiniTemplate', (
				source,
				chunk,
				hash,
				moduleTemplate,
				dependencyTemplates,
			) => {
				this.processTip('compilation.mainTemplate.hooks.render');
				const {name} = chunk;
				if (!this.webpackEntryScriptChunks[name]) return source;
				if (!chunk.hasEntryModule()) return source;

				const sourceBody = compilation.mainTemplate.hooks.modules.call(
					new RawSource(''),
					chunk,
					hash,
					moduleTemplate,
					dependencyTemplates,
				);

				const relativePath = (path) => {
					return replaceDS(pathUtil.relative(pathUtil.dirname(name), `${path}`));
				};

				const modules = this.getRequireModules(chunk);
				const newSource = new ConcatSource();

				newSource.add(`require('./${relativePath((this.options.bootstrapModuleName))}')(${JSON.stringify(chunk.entryModule.id)}, `);

				if (modules.length > 0) {
					newSource.add('Object.assign(');
					modules.forEach(m => {
						newSource.add(`require('./${relativePath(m)}').modules, `);
					});
					newSource.add(sourceBody);
					newSource.add(')');
				} else {
					newSource.add(sourceBody);
				}
				newSource.add(')');

				return newSource;
			});
		});

		compiler.hooks.emit.tapAsync(this.pluginName, (compilation, callback) => {
			this.processTip('compiler.hooks.emit');
			callback();
		});

		compiler.hooks.afterEmit.tapAsync(this.pluginName, (compilation, callback) => {
			this.processTip('compiler.hooks.afterEmit');
			callback();
		});

		compiler.hooks.additionalPass.tapAsync(this.pluginName, (callback) => {
			this.processTip('compiler.hooks.additionalPass');
			this.addEntries(compiler);
			callback();
		});

		compiler.hooks.afterCompile.tap(this.pluginName, (compilation) => {
			this.processTip('compiler.hooks.afterCompile');
		});
	}

	addEntries(compiler) {
		if (this.resources && compiler) {
			const {add, remove} = this.resources.diffEntries();
			add.scripts.forEach(key => {
				const path = this.srcOf(key);
				const chunkName = removeExt(key);
				(new SingleEntryPlugin(this.srcPath, path, chunkName)).apply(compiler);
				this.webpackEntryScripts[key] = path;
				this.webpackEntryScriptChunks[chunkName] = path;
			});

			const mergeAssets = {};
			const assets = add.assets.map(key => {
				mergeAssets[key] = this.srcOf(key);
				return mergeAssets[key];
			});
			const assetsEntry = new MultiEntryPlugin(this.srcPath, assets, this.options.assetsChunkName);
			assetsEntry.apply(compiler);
			_.merge(this.webpackEntryAssets, mergeAssets);
		}
		return this;
	}

	newResources() {
		this.resources = new MiniProgramResources(this, this.resources);
		return this.resources;
	}

	rootOf(...path) {
		return pathUtil.resolve(this.root, ...path);
	}

	srcOf(...path) {
		return pathUtil.resolve(this.srcPath, ...path);
	}

	distOf(...path) {
		return pathUtil.resolve(this.distPath, ...path);
	}

	relativeOfSrc(path) {
		return pathUtil.relative(this.srcPath, path);
	}

	relativeOfRoot(path) {
		return pathUtil.relative(this.root, path);
	}

	relativeOfDist(path) {
		return pathUtil.relative(this.distPath, path);
	}

	makeKey(path, withExt) {
		if (!withExt)
			return replaceDS(removeExt(this.relativeOfSrc(path)));
		else
			return replaceDS(this.relativeOfSrc(path));
	}

	processTip(process, ...subItems) {
		if (!!this.options.debug) {
			let msgs = [`${highLight(this.pluginName)}/${warn(process)}`];
			if (subItems.length > 0)
				msgs = msgs.concat(subItems);
			console.log(...msgs);
		}
		return this;
	}

	isScript(path) {
		return this.isScriptExt(pathUtil.extname(path));
	}

	isScriptExt(ext) {
		return !!this.options.scriptExts[_.trim(ext, '.').toLowerCase()];
	}

	isJson(path) {
		return this.isJsonExt(pathUtil.extname(path));
	}

	isJsonExt(ext) {
		return _.trim(ext, '.').toLowerCase() === 'json';
	}

	getExts(type, key) {
		const {options} = this;
		let exts = Object.assign({}, options.exts);
		if (options[type].exts) {
			Object.assign(exts, options[type].exts);
		}
		if (options.custom[key] && options.custom[key].exts) {
			Object.assign(exts, options.custom[key].exts);
		}
		return filterExts(exts);
	}

	getAdditionFiles(type, key) {
		let files = [];
		const {options} = this;
		if (options.custom[key] && options.custom[key].files && Array.isArray(options.custom[key].files) && options.custom[key].files.length > 0) {
			files.push(...options.custom[key].atta);
		}
		if (options[type] && options[type].files && Array.isArray(options[type].files) && options[type].files.length > 0) {
			files.push(...options[type].files);
		}
		return _.uniq(files);
	}

	addWebpackEntry(item) {
		const path = this.rootOf(item);
		if (this.isScript(path)) {
			const key = this.makeKey(path, true);
			const notExtKey = removeExt(key);
			if (!this.webpackEntryScripts[key])
				this.webpackEntryScripts[key] = path;
			if (!this.webpackEntryScriptChunks[notExtKey])
				this.webpackEntryScriptChunks[notExtKey] = path;
		}
		return this;
	}

	initWebpackEntry(entry) {
		if (_.isString(entry)) {
			this.addWebpackEntry(entry);
		} else {
			_.values(entry).forEach(item => {
				if (Array.isArray(item)) {
					item.forEach(subItem => {
						this.addWebpackEntry(subItem);
					});
				} else if (_.isString(item)) {
					this.addWebpackEntry(item);
				}
			});
		}
		return this;
	}

	getRequireModules(chunk) {
		const modules = [];
		if (chunk.hasEntryModule()) {
			const {id, groupsIterable} = chunk;
			for (const entrypoint of groupsIterable) {
				for (const {name} of entrypoint.chunks) {
					if (name !== chunk.name) {
						modules.push(name);
						// 依赖 chunk 最后被打包的位置
					}
				}
			}
		}
		return modules;
	}
}


module.exports = MiniProgramPlugin;
